﻿using FMOD;
using Groove.Core.Infrastructure;
using Groove.Core.MediaFile;
using System;

namespace Groove.Core.Player
{
    public class FMODPlayerEngine : IPlayerEngine
    {
        private IMediaFile _currentMediaFile;
        public IMediaFile CurrentMediaFile
        {
            get => _currentMediaFile; set
            {
                _currentMediaFile = value;
                InitChannelForMediaFile();
            }
        }

        private PlayerState _playerState;
        public PlayerState PlayerState
        {
            get => _playerState;
            private set
            {
                if (_playerState == value) return;
                _playerState = value;
                StateChanged?.Invoke(this, EventArgs.Empty);
            }
        }

        private uint _currentPosition;
        public uint PositionMs
        {
            get => _currentPosition; 
            set
            {
                if (value > _lengthms) value = _lengthms;
                _currentPosition = value;
                SetCurrentPosition();
            }
        }

        /// <summary>
        /// 音量(0-100)
        /// </summary>
        private float _volumn = 100;
        public float Volumn
        {
            get => _volumn; 
            set
            {
                if (value < 0) value = 0;
                if (value > 100) value = 100;
                _volumn = value;
                SetVolum(value);
            }
        }

        public bool IsMuted { get; private set; }

        public event EventHandler PlayNext;

        public event EventHandler StateChanged;

        public event PlayerPositionChangedDelegate PositionChanged;

        private FMOD.System _playerSystem;
        private Sound _playerSound;
        private Channel _playerChannel;
        private System.Timers.Timer _timer;
        private bool _hasInited;

        private uint _lengthms;
        // 需定义为成员变量处理C++程序的回调，否则会被垃圾回收，导致C++回调找不到指针
        // ReSharper disable once NotAccessedField.Local
        private PlayerChannelCallbackHandler _playerChannelCallbackHandler;

        public bool Init()
        {
            if (_hasInited) return true;
            try
            {
                if (!Factory.System_Create(out _playerSystem).ERRCHECK())
                {
                    return false;
                }
                _playerSystem.getVersion(out uint version).ERRCHECK();
                if (version < VERSION.number)
                {
                    return false;
                }

                if (!_playerSystem.init(16, INITFLAGS.NORMAL, (IntPtr)null).ERRCHECK())
                {
                    return false;
                }

                if (!_playerSystem.setStreamBufferSize(128 * 1024, TIMEUNIT.RAWBYTES).ERRCHECK())
                {
                    return false;
                }

                //Set Timer
                _timer = new System.Timers.Timer
                {
                    Interval = 10
                };
                _timer.Elapsed += PlayTimerCallback;

                _hasInited = true;
                return _hasInited;
            }
            catch (Exception ex)
            {
                LogWriter.Error("Init FMOD Player Failed", ex);
                return false;
            }
        }

        public bool PlayOrPause()
        {
            if (CurrentMediaFile == null) return false;
            if (PlayerState == PlayerState.Stopped)
                InitChannelForMediaFile();
            var isPause = PlayerState == PlayerState.Playing;
            if (isPause)
                _timer.Stop();
            else
                _timer.Start();
            _playerChannel.setPaused(isPause);
            _playerSystem.update();
            PlayerState = isPause ? PlayerState.Paused : PlayerState.Playing;
            return true;
        }

        public bool Stop()
        {
            _timer.Stop();
            CleanUpSound();
            _lengthms = 0;
            PositionMs = 0;
            PlayerState = PlayerState.Stopped;
            return true;
        }

        public bool Mute()
        {
            IsMuted = !IsMuted;
            SetMuteState();
            return true;
        }

        public bool ShutDown()
        {
            if (!_hasInited) return true;
            Stop();            
            CleanUpSystem();
            _hasInited = false;
            return true;
        }

        private void InitChannelForMediaFile()
        {
            Stop();
            // Create sound
            var mode = MODE.DEFAULT | MODE._2D | MODE.CREATESTREAM | MODE.LOOP_OFF;
            if (!_playerSystem.createSound(CurrentMediaFile.FullFileName, mode, out _playerSound).ERRCHECK())
            {
                return;
            }
            _playerSound.getLength(out _lengthms, TIMEUNIT.MS).ERRCHECK();
            // Create channel, start paused
            _playerSystem.getMasterChannelGroup(out ChannelGroup masterChannelGroup).ERRCHECK();
            if (!masterChannelGroup.hasHandle())
            {
                return;
            }
            if (!_playerSystem.playSound(_playerSound, masterChannelGroup, true, out _playerChannel).ERRCHECK())
            {
                return;
            }
            if (!_playerChannel.hasHandle())
            {
                return;
            }
            _playerChannelCallbackHandler = new PlayerChannelCallbackHandler(_playerChannel, RaisePlayNext);

            // Init params
            SetVolum(Volumn);
            SetMuteState();            
        }

        private void CleanUpSound()
        {
            if (_playerChannel.hasHandle())
            {
                _playerChannel.setPaused(true);
                _playerChannel.setCallback(null);
                _playerChannel.clearHandle();
            }
            if (_playerSound.hasHandle())
            {
                _playerSound.release();
                _playerSound.clearHandle();
            }
            _playerSystem.update().ERRCHECK();
        }

        private void CleanUpSystem()
        {
            _timer.Dispose();
            if (!_playerSystem.hasHandle()) return;
            _playerSystem.close().ERRCHECK();
            _playerSystem.release().ERRCHECK();
            _playerSystem.clearHandle();
        }

        private void SetVolum(float value)
        {
            if (!_playerChannel.hasHandle()) return;
            _playerChannel.setVolume(value/100f);
            _playerSystem.update();            
        }

        private void SetMuteState()
        {
            if (!_playerChannel.hasHandle()) return;
            _playerChannel.setMute(IsMuted);
            _playerSystem.update();
        }

        private void RaisePlayNext()
        {
            PlayerState = PlayerState.Stopped;
            PlayNext?.Invoke(this, EventArgs.Empty);
        }

        private void SetCurrentPosition()
        {
            if (!_playerChannel.hasHandle()) return;
            _playerChannel.setPosition(PositionMs, TIMEUNIT.MS);
            _playerSystem.update();
            if (PlayerState != PlayerState.Playing)
            {
                PlayOrPause();
            }
        }

        private void PlayTimerCallback(object sender, EventArgs e)
        {
            if (!_playerChannel.hasHandle()) return;
            _playerChannel.getPosition(out uint ms, TIMEUNIT.MS);
            _currentPosition = ms;
            PositionChanged?.Invoke(PositionMs);
            _playerSystem.update();
        }
    }

    internal class PlayerChannelCallbackHandler
    {
        private readonly Action _playNextFileAction;

        // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
        // Create field to avoid GC;
        private readonly CHANNELCONTROL_CALLBACK _channelEndCallback;

        public PlayerChannelCallbackHandler(Channel channel, Action playNextFileAction)
        {
            _playNextFileAction = playNextFileAction;
            _channelEndCallback = ChannelEndCallback;
            channel.setCallback(_channelEndCallback).ERRCHECK();
        }

        private RESULT ChannelEndCallback(IntPtr channelcontrol, CHANNELCONTROL_TYPE controltype, CHANNELCONTROL_CALLBACK_TYPE callbacktype, IntPtr commanddata1, IntPtr commanddata2)
        {
            if (callbacktype == CHANNELCONTROL_CALLBACK_TYPE.END)
            {
                _playNextFileAction?.Invoke();

                // this must be thread safe
                //var currentSynchronizationContext = TaskScheduler.FromCurrentSynchronizationContext();
                //var uiTask = Task.Factory.StartNew(() => {
                //    _playNextFileAction?.Invoke();
                //}, CancellationToken.None, TaskCreationOptions.None, currentSynchronizationContext);
            }
            return RESULT.OK;
        }
    }
}
